using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Xml;
using System.Html.HtmlTags;
using System.Html.HtmlElements;
using System.Diagnostics;
using System.Security;
using System.Collections;

namespace System.Html {
    public class HtmlParser {

        private const string TAG_NAMESPACE = "System.Html.HtmlTags.";
        private const RegexOptions PARSE_OPTIONS = RegexOptions.IgnoreCase | RegexOptions.Multiline;
        internal const string XhtmlNamespace = "http://www.w3.org/1999/xhtml";

        public bool IgnoreNamespaces {
            get;
            set;
        }

        private XmlDocument OwnerDocument {
            get;
            set;
        }

        public HtmlParser() {

        }

        public void Parse(XmlDocument doc, string html) {
            OwnerDocument = doc;
            //break up the html into tokens
            List<HtmlToken> _tokens = Tokenize(html);
            if (doc.DocumentElement == null) {
                doc.LoadXml(Properties.Resources.HtmlDocumentBase);
            }
            ProcessTag(doc.DocumentElement, _tokens);
        }

        public XmlDocument Parse(string html) {
            XmlDocument _parsedHtml = new XmlDocument();
            _parsedHtml.LoadXml(Properties.Resources.HtmlDocumentBase);
            Parse(_parsedHtml, html);
            return _parsedHtml;
        }

        private List<XmlNode> GenerateNodeList(XmlNode rootNode) {
            List<XmlNode> _nodeList = new List<XmlNode>();
            foreach (XmlNode _node in rootNode.ChildNodes) {
                _nodeList.Add(_node);
                _nodeList.AddRange(GenerateNodeList(_node));
            }
            return _nodeList;
        }
        /// <summary>
        /// Processes the text input into tags
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="tokens"></param>
        private void ProcessTag(XmlNode parent, List<HtmlToken> tokens) {
            Stack<XmlNode> _openTags = new Stack<XmlNode>();
            _openTags.Push(parent);
            foreach (HtmlToken _t in tokens) {
                if (_t.TokenType == HtmlTokenTypes.Text) {
                    XmlNode _textNode = parent.OwnerDocument.CreateCDataSection(SecurityElement.Escape(_t.ToString()));
                    _openTags.Peek().AppendChild(_textNode);
                }
                else if (_t.TokenType == HtmlTokenTypes.Tag) {
                    XmlNode _tag = OwnerDocument.CreateElement(_t.TagName.ToLower());
                    ProcessTagAttributes(_t, _tag);
                    _openTags.Peek().AppendChild(_tag);
                    if (!_t.IsSelfClosingTag) {
                        _openTags.Push(_tag);
                    }
                }
                else if (_t.TokenType == HtmlTokenTypes.CloseTag) {
                    if (_openTags.Peek().Name == _t.TagName) {
                        _openTags.Pop();
                    }
                    else {
                        //encountered a close tag that didn't match the current open tag
                        //add as cdata
                        XmlNode _textNode = parent.OwnerDocument.CreateCDataSection(SecurityElement.Escape(_t.ToString()));
                        _openTags.Peek().AppendChild(_textNode);
                    }
                }
            }
        }
        /// <summary>
        /// Breaks up the html into tokens for xml building
        /// </summary>
        /// <param name="html"></param>
        /// <returns></returns>
        private List<HtmlToken> Tokenize(string html) {
            List<HtmlToken> _tokens = new List<HtmlToken>();
            int[] _openers = ScanFor('<', html);
            int[] _closers = ScanFor('>', html);
            if (_openers[0] > 0) {
                _tokens.Add(new HtmlToken(html.Substring(0, _openers[0]), HtmlTokenTypes.Text));
            }
            for (int i = 0; i < _openers.Length; i++) {
                if (html[_openers[i] + 1] == '/') {
                    _tokens.Add(new HtmlToken(GetSection(html, _openers[i], NextCloser(_openers[i],_closers)), HtmlTokenTypes.CloseTag));
                }
                else {
                    if (html[_openers[i] + 1] != '!') {
                        HtmlToken _tag = new HtmlToken(GetSection(html, _openers[i], NextCloser(_openers[i], _closers)), HtmlTokenTypes.Tag);
                        _tokens.Add(_tag);
                        if (_tag.TagName == "script") {
                            for (int j = i + 1; j < _openers.Length; j++) {
                                if (html.Substring(_openers[j], 8) == "</script") {
                                    _tokens.Add(new HtmlToken(GetSection(html, NextCloser(_openers[i], _closers) + 1, _openers[j] - 1), HtmlTokenTypes.Text));
                                    _tokens.Add(new HtmlToken("</script>", HtmlTokenTypes.CloseTag));
                                    i = j;
                                    break;
                                }
                            }
                        }
                    }
                    else {
                        _tokens.Add(new HtmlToken(GetSection(html, _openers[i], NextCloser(_openers[i], _closers)), HtmlTokenTypes.Text));
                    }
                }
                if (_openers.Length > i + 1) {
                    string _text = GetSection(html, NextCloser(_openers[i], _closers) + 1, _openers[i + 1] - 1);
                    if (!String.IsNullOrEmpty(_text)) {
                        _tokens.Add(new HtmlToken(_text, HtmlTokenTypes.Text));
                    }
                }
                else {
                    _tokens.Add(new HtmlToken(GetSection(html, NextCloser(_openers[i], _closers) + 1, html.Length - 1), HtmlTokenTypes.Text));
                }
            }
            return _tokens;
        }
        private int NextCloser(int position, int[] closers) {
            for (int i = 0; i < closers.Length; i++) {
                if (closers[i] > position) {
                    return closers[i];
                }
            }
            return position;
        }

        private string GetSection(string data, int start, int end) {
            return data.Substring(start, (end - start) + 1);
        }
        private int[] ScanFor(char scan, string html) {
            List<int> _positions = new List<int>();
            for (int i = 0; i < html.Length; i++) {
                if (html[i] == '\'') {
                    while (i + 1 < html.Length) {
                        i++;
                        if (html[i] == '\'') {
                            if (html[i - 1] != '\\') {
                                break;
                            }
                        }
                    }
                }
                if (html[i] == '"') {
                    while (true) {
                        i++;
                        if (html[i] == '"') {
                            if (html[i - 1] != '\\') {
                                break;
                            }
                        }
                    }
                }
                if (html[i] == scan) {
                    _positions.Add(i);
                }
            }
            return _positions.ToArray();
        }

        /// <summary>
        /// Creates the xml attributes for the tag
        /// </summary>
        /// <param name="tagToken"></param>
        /// <param name="tag"></param>
        private void ProcessTagAttributes(HtmlToken tagToken, XmlNode tag) {
            //extract the attributes
            string _attributes = tagToken.Attributes;
            const string _attributeExpression = @"(?<Attribute>((([\w-:]+\s*=\s*\\?""[^""]*\\?""+)|([\w-:]+\s*=\s*'[^""]*""[^""]*"")|([\w-:]+\s*=\s*'[^']*')|[\w-:]+=[^\s>]+)|([\w-:]+))((\s*)|(/*>)))*";
            const RegexOptions _options = RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled;
            Regex _attributeRegEx = new Regex(_attributeExpression, _options);
            //for each match we need to extract the name and value
            Match _match = _attributeRegEx.Match(_attributes);
            for (int _counter = 0; _counter < _match.Groups["Attribute"].Captures.Count; _counter++) {
                //process each name and value
                Capture _attributeCapture = _match.Groups["Attribute"].Captures[_counter];
                string _attributeText = _attributeCapture.Value;
                //break up the attributes
                int _seperatorIndex = _attributeText.IndexOf('=');
                XmlAttribute _attrib = null;
                if (_seperatorIndex > 0) {
                    string _attName = _attributeText.Substring(0, _seperatorIndex).Trim().ToLower();
                    string _attValue = _attributeText.Substring(_attributeText.IndexOf('=') + 1).Trim();
                    if (_attValue.IndexOf('"') >= 0) {
                        _attValue = _attValue.Substring(_attValue.IndexOf('"') + 1);
                        if (_attValue.IndexOf('"') >= 0) {
                            if (_attValue.IndexOf("\\\"") > 0) {
                                _attValue = _attValue.Substring(0, _attValue.IndexOf('"') - 1);
                            }
                            else {
                                _attValue = _attValue.Substring(0, _attValue.IndexOf('"'));
                            }
                        }
                    }
                    else if (_attValue.IndexOf('\'') >= 0) {
                        _attValue = _attValue.Substring(_attValue.IndexOf('\'') + 1);
                        _attValue = _attValue.Substring(0, _attValue.IndexOf('\''));
                    }
                    try {
                        _attrib = tag.OwnerDocument.CreateAttribute(_attName);
                        _attrib.Value = _attValue;
                    }
                    catch (XmlException xmlEx) { }
                }
                else {
                    string _attName = _attributeText.Trim().ToLower();
                    try {
                        _attrib = tag.OwnerDocument.CreateAttribute(_attName);
                        _attrib.Value = _attName;
                    }
                    catch (XmlException xmlEx) { }
                }
                if (_attrib != null) {
                    if (IgnoreNamespaces) {
                        if (_attrib.Name != "xmlns") {
                            tag.Attributes.Append(_attrib);
                        }
                    }
                    else {
                        tag.Attributes.Append(_attrib);
                    }
                }
            }
        }


        private static Hashtable tagCache = new Hashtable();
        /// <summary>
        /// Gets an instance of a html tag
        /// </summary>
        /// <param name="tagName"></param>
        /// <returns></returns>
        public static Tag GetTagInstance(string tagName) {
            if (tagCache.ContainsKey(tagName)) {
                return (Tag)tagCache[tagName];
            }
            else {
                //get an instance of this tag
                Type _tagType = Type.GetType(TAG_NAMESPACE + tagName, true, true);
                Tag _htmlTag = (Tag)_tagType.GetConstructor(Type.EmptyTypes).Invoke(null);
                //cache it and tag it
                tagCache.Add(tagName, _htmlTag);
                //and return it
                return _htmlTag;
            }
        }
        /// <summary>
        /// Used to store tokens
        /// </summary>
        private class HtmlToken {
            protected string tokenText;
            protected string attributeText = string.Empty;
            protected string tagName;
            HtmlTokenTypes tokenType;

            public HtmlTokenTypes TokenType {
                get {
                    return this.tokenType;
                }
            }

            public string TagName {
                get {
                    if (this.tagName == null &&
                         (this.tokenType == HtmlTokenTypes.Tag ||
                            this.tokenType == HtmlTokenTypes.CloseTag)
                        ) {
                        //extract the tag name
                        string _tagExpression = string.Empty;
                        if (this.tokenType == HtmlTokenTypes.Tag) {
                            _tagExpression = @"<(?<TagName>[\w-]+)\s*";
                        }
                        else if (this.tokenType == HtmlTokenTypes.CloseTag) {
                            _tagExpression = @"</\s*(?<TagName>[\w-]+)\s*>";
                        }
                        const RegexOptions _options = RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled;
                        Regex _tagRegEx = new Regex(_tagExpression, _options);
                        Match _match = _tagRegEx.Match(tokenText);
                        this.tagName = _match.Groups["TagName"].Value;
                    }
                    return this.tagName;
                }
            }

            public bool IsSelfClosingTag {
                get {
                    if(tokenType == HtmlTokenTypes.Tag){
                        if (tokenText[tokenText.Length - 2] =='/') {
                            return true;
                        }
                        else {
                            return false;
                        }
                    }
                    else {
                        return false;
                    }
                }
            }

            public string Attributes {
                get {
                    return this.attributeText;
                }
            }

            public HtmlToken(string tokenText, HtmlTokenTypes tokenType) {
                this.tokenText = tokenText;
                this.tokenType = tokenType;
                if (tokenType == HtmlTokenTypes.Tag) {
                    int _space = tokenText.IndexOf(' ');
                    if (_space != -1) {
                        int _length = tokenText.Length - (_space + 1);
                        if (tokenText[tokenText.Length - 2] == '/') {
                            _length -= 1;
                        }
                        attributeText = tokenText.Substring(_space, _length).Trim();
                    }
                }
            }

            public HtmlToken(string tokenText, HtmlTokenTypes tokenType, string attributes) {
                this.tokenText = tokenText;
                this.tokenType = tokenType;
                this.attributeText = attributes;
            }

            public override string ToString() {
                return this.tokenText;
            }
        }
        /// <summary>
        /// Enum of the different kinds of tokens
        /// </summary>
        private enum HtmlTokenTypes {
            Tag = 1,
            Text = 2,
            CloseTag = 3
        }
    }
}